#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Xml;

using Osh.Media;
using ProgDVB;

namespace Osh.ProgDvb
{
	public delegate void SettingsChangedHandler(ProgDvbApplication app);

	/// <summary>
	/// 
	/// </summary>
	public class ProgDvbApplication : MediaPlayerBase,
		Osh.Dvb.IDvbPlayer,
		Osh.Dvb.IDvbPacketStreamSource,
		Osh.Dvb.IEpgStreamSource,
		Osh.Teletext.ITeletextStreamSource
	{
		private const int PlayerMonitoringIntervalMs = 1000; // ms
		private static TraceSource _traceSource = new TraceSource("Osh.ProgDvb");
		private ProgDVB.ProgDVB _progDvb;
		private ChannelDatabase _channelDatabase;
		private Settings _settings;
		private PlaybackMonitor _playbackMonitor;
		private Timeline _timeline;
		private Channel _currentChannel;
		private string _progDvbFolder;
		private TimelineInfo _timelineInfo;

		public ProgDvbApplication(string name) : base(name)
		{
			EnsureProgDvbIsInstalled();

			_timeline = new Timeline(this);

			Osh.Configuration.Configuration.Current.Changed +=
				new Osh.Configuration.ConfigurationEventHandler(OshConfiguration_Changed);
			OshConfiguration_Changed(null, null);

			base.PlayerMonitoringInterval = PlayerMonitoringIntervalMs;
		}

		private IProgDVB GetProgDvb(bool checkItAlive)
		{
			if((_progDvb != null) && checkItAlive)
			{
				try
				{
					string version = _progDvb.Version; // ping ProgDVB
				}
				// When ProgDVB get closed this can be BadImageFormatException (not just COMException)
				// with the error message: Invalid access to memory location. (Exception from HRESULT: 0x800703E6)
				catch(SystemException)
				{
					Marshal.ReleaseComObject(_progDvb);
					_progDvb = null;
				}
			}
			if(_progDvb == null)
			{
				try
				{
					_progDvb = (ProgDVB.ProgDVB)Marshal.GetActiveObject("OSH.ProgDVB");
					_progDvb.OnChannelChange += new _IProgDVBEvents_OnChannelChangeEventHandler(ProgDvb_OnChannelChange);
				}
				catch(SystemException)
				{
					if(_progDvb != null)
					{
						Marshal.ReleaseComObject(_progDvb);
						_progDvb = null;
					}
				}
			}
			return _progDvb;
		}

		private void SendCommand(string command)
		{
			try
			{
				IProgDVB progDvb = GetProgDvb(true);
				if(progDvb != null)
					progDvb.SendCommand(command);
			}
			catch(SystemException) { }
		}

		#region Osh.Media.IMediaPlayer
		public override void PlayMedia(IMedia media)
		{
			try
			{
				IProgDVB progDvb = GetProgDvb(true);
				if(progDvb != null)
					progDvb.SetChannel((media as Channel).IdString);
			}
			catch(SystemException) { }
		}

		public override void Play()
		{
			SendCommand("Pause"); // Play commnand is not supported by the player
		}

		public override void Pause()
		{
			SendCommand("Pause");
		}

		public override void Stop()
		{
			SendCommand("Stop");
		}

		public override ITimeline Timeline
		{
			get { return _timeline; }
		}
		#endregion Osh.Media.IMediaPlayer

		public void AddPacketFilter(string name, ushort pid, uint maxBufferSize, uint maxPacketSize, bool checkCrc32)
		{
			IProgDVB progDvb = GetProgDvb(true);
			if(progDvb == null)
				throw new InvalidOperationException();

			try
			{
				progDvb.AddFilter(name, pid, maxBufferSize, maxPacketSize, checkCrc32);
			}
			catch(COMException e)
			{
				if(e.ErrorCode != Win32.GetHRForWin32Error(Win32.ERROR_ALREADY_EXISTS))
					throw e;
			}
		}

		public void RemovePacketFilter(string name)
		{
			IProgDVB progDvb = GetProgDvb(true);
			if(progDvb == null)
				throw new InvalidOperationException();

			progDvb.RemoveFilter(name);
		}

		#region Osh.Dvb.IDvbPlayer

		#endregion Osh.Dvb.IDvbPlayer

		#region Osh.Epg.IEpgSource Members
		public PacketStream OpenEpgStream()
		{
		    return new Osh.Dvb.PsiStream(new ProgDvbPacketStream(this, (ushort)Osh.Dvb.ServiceInformationPids.EIT, false), true);
		}
		#endregion Osh.Epg.IEpgSource Members

		#region Osh.Teletext.ITeletextStreamSource Members
		public PacketStream OpenTeletextStream()
		{
		    if(this.CurrentChannel == null)
		        throw new InvalidOperationException("Cannot open teletext stream. Channel is not set.");

		    ushort teletextPid = this.CurrentChannel.TeletextId;
		    if(teletextPid == 0)
		        throw new InvalidOperationException("Cannot open teletext stream. Channel's teletext PID is unknown.");

			return new Osh.Dvb.PesStream(new ProgDvbPacketStream(this, teletextPid, true));
		}
		#endregion Osh.Teletext.ITeletextStreamSource Members

		#region IDvbPacketStreamSource Members
		public Stream OpenPacketStream(Osh.Dvb.PacketStreamType type, ushort pid)
		{
			return new ProgDvbPacketStream(this, pid, true);
		}
		#endregion IDvbPacketStreamSource Members

		protected override PlayerState GetPlayerState()
		{
			string stateStr = null;
			try
			{
				IProgDVB progDvb = GetProgDvb(false);
				if(progDvb == null)
					return PlayerState.Closed;

				stateStr = progDvb.GetState(GetStateEnum.progDvbStateTimeline);
			}
			catch
			{
				GetProgDvb(true); // Release COM object reference.
			}

			return ParseState(stateStr);
		}

		private PlayerState ParseState(string stateXml)
		{
			if(string.IsNullOrEmpty(stateXml))
				return PlayerState.Closed;

			PlayerState playerState = PlayerState.Ready;

			using(XmlReader reader = XmlReader.Create(new StringReader(stateXml)))
			{
				while(reader.Read())
				{
					if(reader.NodeType == XmlNodeType.Element && reader.Name == "Playback")
					{
						string stateStr = reader.GetAttribute("state").ToLowerInvariant();
						if(stateStr == "playing")
							playerState = PlayerState.Playing;
						else if(stateStr == "paused")
							playerState = PlayerState.Paused;
						else if(stateStr == "stoped")
							playerState = PlayerState.Ready;
						else // "unknown"
							playerState = PlayerState.Ready;
					}

					if(reader.NodeType == XmlNodeType.Element && reader.Name == "Timeline")
					{
						DateTime? startTime, endTime, currentTime;
						startTime = endTime = currentTime = null;

						if(reader.MoveToAttribute("start"))
							startTime = reader.ReadContentAsDateTime();

						if(reader.MoveToAttribute("end"))
							endTime = reader.ReadContentAsDateTime();

						if(reader.MoveToAttribute("current"))
							currentTime = reader.ReadContentAsDateTime();

						if(startTime != null && endTime != null && currentTime != null)
							_timelineInfo = new TimelineInfo(startTime.Value, endTime.Value, currentTime.Value);
					}
				}
			}
			return playerState;
		}

		protected override TraceSource TraceSourceInternal
		{
			get { return ProgDvbApplication.TraceSource; }
		}
		
		public static TraceSource TraceSource
		{
			get { return _traceSource; }
		}

		public Settings Settings
		{
			get { return _settings; }
		}

		public ChannelDatabase ChannelDatabase
		{
			get { return _channelDatabase; }
		}

		private ChannelId ParseChannelId(string channelId)
		{
			ChannelId result = ChannelId.Invalid;
			ChannelId.TryParse(channelId, out result);
			return result;
		}

		public ChannelId CurrentChannelId
		{
			get
			{
				ChannelId channelId = ChannelId.Invalid;
				try
				{
					IProgDVB progDvb = GetProgDvb(true);
					if(progDvb != null)
					{
						IChannelInfo channelInfo = progDvb.Channel;
						if(channelInfo != null)
						{
							channelId = ParseChannelId(channelInfo.ID);
							Marshal.ReleaseComObject(channelInfo);
						}
					}

				}
				catch(SystemException) { }

				return channelId;
			}
		}

		public Channel CurrentChannel
		{
			get
			{
				if(_currentChannel == null && _channelDatabase != null)
				{
					ChannelId channelId = this.CurrentChannelId;
					if(channelId != ChannelId.Invalid)
						_channelDatabase.TryGetChannel(channelId, out _currentChannel);
				}
				return _currentChannel;
			}
		}

		internal TimelineInfo TimelineInfo
		{
			get { return _timelineInfo; }
		}

		public void TakeScreenshot()
		{
			SendCommand("ScreenShot");
		}

		private void EnsureProgDvbIsInstalled()
		{
			string progDvbFolder = Osh.Configuration.Configuration.Current.ProgDvb.RootFolder;
			if(!Directory.Exists(progDvbFolder))
			{
				throw new PlayerNotFoundException(typeof(ProgDvbApplication),
					string.Format("Cannot find ProgDVB folder '{0}'. " +
						"Make sure ProgDVB is installed and OSH configuration is correct.", progDvbFolder));
			}

			string channelsFolder = Path.Combine(progDvbFolder, "Channels");
			if(!ChannelDatabase.Exists(channelsFolder))
				throw new PlayerNotFoundException(typeof(ProgDvbApplication),
					string.Format("Cannot find ProgDVB channel database at '{0}'. " +
						"Make sure ProgDVB is properly installed and OSH configuration is correct.", channelsFolder));
		}

		private void OshConfiguration_Changed(object sender, EventArgs args)
		{
			string progDvbFolder = Osh.Configuration.Configuration.Current.ProgDvb.RootFolder;
			if(_progDvbFolder != null && _progDvbFolder == progDvbFolder)
				return;

			if(_channelDatabase != null)
			{
				OshHostBase.Current.Osh.MediaController.UnregisterMediaLibrary(_channelDatabase);
				_channelDatabase = null;
				base.Services.UnregisterService(typeof(IMediaLibrary));
			}

			if(!string.IsNullOrEmpty(progDvbFolder))
			{
				_settings = new Settings(this, progDvbFolder);
				_channelDatabase = new ChannelDatabase(Path.Combine(progDvbFolder, "Channels"));
				OshHostBase.Current.Osh.MediaController.RegisterMediaLibrary(_channelDatabase);
				base.Services.RegisterService(typeof(IMediaLibrary), _channelDatabase);

				_playbackMonitor = new PlaybackMonitor(this);
				base.Services.RegisterService(typeof(IProgramScreenshotProvider), _playbackMonitor);
			}
			else
			{
				if(_playbackMonitor != null && _playbackMonitor.Enabled)
					_playbackMonitor.Enabled = false;
				_playbackMonitor = null;
				base.Services.UnregisterService(typeof(IProgramScreenshotProvider));

				_settings = null;
			}

			_progDvbFolder = progDvbFolder;
		}

		private void ProgDvb_OnChannelChange(string channelIdStr)
		{
			_currentChannel = null;

			ChannelId channelId = ParseChannelId(channelIdStr);
			if(channelId != ChannelId.Invalid)
				_channelDatabase.TryGetChannel(channelId, out _currentChannel);

			base.CurrentMedia = _currentChannel; // Set the media and forse MediaChange event
		}

		internal void OnSettingsChanged()
		{
			if(this.SettingsChanged != null)
				this.SettingsChanged(this);
		}

		public event SettingsChangedHandler SettingsChanged;

	}
}
